﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

using Microsoft.Xna.Framework;
using TiledLib;

namespace DarwinIsDead {
    /// <summary>
    /// Manages <see cref="Zombie"/> generation and <see cref="Trait"/>
    /// distribution.
    /// </summary>
    class ZombieFactory : IDisposable {
        private World world;
        private Player player;
        private Dictionary<int, Trait> genome;
        private Dictionary<int, int> levels;
        private List<Zombie> zombies;

        private float totalDistance;
        private float avgDistance;  // Average distance traveled by wave
        private float varDistance;  // Variance of distance traveled by wave
        private float distanceOffset;

        private int dnaSize;
        private int waveSize;
        private int numFittest;

        public int DeadZombies { get; private set; }
        public int NumZombies {
            get { return zombies.Count; }
        }
        public int Wave { get; private set; }

        /// <summary>
        /// Create a <see cref="ZombieFactory"/> with the given specifications.
        /// </summary>
        /// <remarks>
        /// </remarks>
        /// <param name="world">Reference to the World; for managing entities
        /// </param>
        /// <param name="player">Reference to player</param>
        /// <param name="dnaSize">Number of traits a zombie has</param>
        /// <param name="waveSize">Number of zombies to spawn per wave</param>
        /// <param name="percentFittest">Percent of zombie wave to
        /// carry into next generation</param>
        public ZombieFactory(World world, Player player, int dnaSize,
                int waveSize, float percentFittest) {
            this.world = world;
            this.player = player;
            genome = new Dictionary<int, Trait>();
            levels = new Dictionary<int, int>();
            zombies = new List<Zombie>();

            this.dnaSize = dnaSize;
            this.waveSize = waveSize;
            numFittest = (int)(waveSize * percentFittest + 0.5f);

            totalDistance = 0f;
            avgDistance = 0f;
            varDistance = 0f;
            distanceOffset = 0.01f;
            DeadZombies = 0;
            Wave = 0;
        }

        /// <summary>
        /// Dispose of Zombie Factory.
        /// </summary>
        public void Dispose() {
            foreach (Zombie z in zombies) {
                world.RemoveEntity(z);
                z.Dispose();
            }
        }

        public void Reset() {
            Dispose();
            zombies.Clear();
            foreach (int k in new List<int>(levels.Keys)) {
                levels[k] = 1;
            }
            DeadZombies = 0;
        }

        /// <summary>
        /// Adds a trait to the genome.
        /// </summary>
        /// <remarks>
        /// 
        /// </remarks>
        /// <param name="trait"></param>
        public void RegisterTrait(Trait trait) {
            int hash = trait.GetType().GetHashCode();
            genome.Add(hash, trait);
            levels.Add(hash, 1);
        }

        /// <summary>
        /// Fitness function.
        /// </summary>
        /// <remarks>
        /// Returns the effectiveness of a <see cref="Zombie"/> instance, based
        /// on how much damage it incurred per second.  The results are used to
        /// find which Zombies are worthy of reproducing.
        /// </remarks>
        /// <param name="zombie"></param>
        /// <returns></returns>
        private int Fitness(Zombie zombie) {
            // Linear damage weight
            float damageWeight = ((float)zombie.TotalDamage / player.MaxHealth) *
                ((float)zombie.TotalDamage / zombie.Lifetime);

            // Gaussian distance weight
            float dd = zombie.Distance - totalDistance;
            float distanceWeight = (float)((1 / Math.Sqrt(2 * Math.PI * varDistance)) *
                Math.Exp(-(dd * dd) / (2 * varDistance))) + distanceOffset;

            return (int)(distanceWeight + damageWeight / zombie.Lifetime + 0.5f);
        }

        private int ZombieCompare(Zombie z1, Zombie z2) {
            float f1 = Fitness(z1);
            float f2 = Fitness(z2);

            if (z1 == z2 || f1 == f2) {
                return 0;
            } else if (f2 > f1) {
                return 1;
            } else {
                return -1;
            }
        }

        public void InitialWave() {
            List<int> pool = new List<int>();
            foreach (int key in genome.Keys) {
                pool.Add(key);
            }

            SpawnWave(pool, 0);
        }

        /// <summary>
        /// Called when the kill count exceeds the size of the zombie array
        /// </summary>
        public void GenerateWave(int time) {
            Wave++;
            Debug.WriteLine("Wave #: " + Wave);

            // Calculate distance statistics
            totalDistance = 0f;
            for (int i = 0; i < zombies.Count; i++) {
                totalDistance += zombies[i].Distance;
            }
            avgDistance = totalDistance / zombies.Count;

            varDistance = 0f;
            for (int i = 0; i < zombies.Count; i++) {
                varDistance += (float)Math.Pow(zombies[i].Distance - avgDistance, 2);
            }
            varDistance /= zombies.Count;

            Debug.WriteLine("Total distance: " + totalDistance);
            Debug.WriteLine("Average distance: " + avgDistance);
            Debug.WriteLine("Distance variance: " + varDistance);
            
            // Sort zombie list according to fitness
            zombies.Sort(ZombieCompare);

            int totalDamage = 0;
            for (int i = 0; i < zombies.Count; i++) {
                totalDamage += zombies[i].TotalDamage;
            }
            Debug.WriteLine("Total damage: " + totalDamage);

            // Retrieve the highest-achieving zombies
            List<Zombie> newBreed = zombies.GetRange(0, numFittest);

            // Pool together successful Traits, culling redundant ones

            // Convergence problem: We're randomly choosing Traits, so there's
            // a chance we're excluding some and over-including others.  This
            // decreases the pool of possible Traits in the next generation,
            // causing an infinite loop somewhere.

            // Solution:
            //     -Randomly add several other traits from the genome
            List<int> traitPool = new List<int>();
            for (int i = 0; i < newBreed.Count; i++) {
                List<Trait> traits = newBreed[i].Traits;
                for (int j = 0; j < traits.Count; j++) {
                    Type traitType = traits[j].GetType();
                    int hash = traitType.GetHashCode();
                    if (!traitPool.Contains(hash)) {
                        traitPool.Add(hash);
                        
                        // Strengthen the trait in the genome
                        if (genome.ContainsKey(hash) &&
                                levels.ContainsKey(hash)) {
                            levels[hash]++;
                        }
                    }
                }
            }

            // If there's room in the trait pool, randomly add other traits
            // in the genome
            if (traitPool.Count < dnaSize) {
                Random rng = new Random();
                List<int> keys = genome.Keys.ToList();

                while (traitPool.Count < dnaSize) {
                    int key = keys[traitPool.Count];
                    if (!traitPool.Contains(key)) {
                        traitPool.Add(key);
                    }
                }
            }

            Debug.Write("Trait pool: ");
            for (int i = 0; i < traitPool.Count; i++) {
                Debug.Write(genome[traitPool[i]].GetType().Name + " ");
            }
            Debug.Write("\n");

            // Remove current wave from world
            for (int i = 0; i < zombies.Count; i++) {
                world.RemoveEntity(zombies[i]);
                zombies[i].Dispose();
            }
            zombies.Clear();

            // Mating and birthing phase
            SpawnWave(traitPool, time);
        }

        private void SpawnWave(List<int> traitPool, int time) {
            Random rng = new Random();

            for (int i = 0; i < waveSize; i++) {
                List<int> tempPool = new List<int>(traitPool);

                Zombie zombie = new Zombie(world);
                zombie.KillEvent = KillZombie;

                for (int j = 0; j < dnaSize; j++) {
                    // Choose random trait from pool
                    int poolIndex = rng.Next(tempPool.Count);
                    int traitKey = tempPool[poolIndex];

                    // Remove trait from current zombie's pool so it doesn't
                    // receive redundant traits
                    tempPool.Remove(traitKey);

                    // Get copy of Trait from genome
                    Trait trait = (Trait)genome[traitKey].Clone();
                    trait.Owner = zombie;
                    trait.Level = levels[traitKey];

                    // Add trait to zombie
                    zombie.AddTrait(trait);
                }

                // Initialize zombie's traits
                for (int j = 0; j < zombie.Traits.Count; j++) {
                    zombie.Traits[j].Initialize();
                    zombie.Traits[j].Enabled = true;
                }

                // Add zombie to zombie list
                zombies.Add(zombie);

                // Add Zombie to world
                world.AddEntity(zombie);

                // Spawn in specified spawnRegions
                //List<MapObject> spawnRegions = world.Background.getZombieSpawn();
                //Rectangle spawnRegion = spawnRegions[rng.Next(spawnRegions.Count)].Bounds;
                zombie.Spawn(rng.Next(640), rng.Next(480), time);

            }
        }

        public void KillZombie(int time) {
            DeadZombies++;
            if (DeadZombies >= zombies.Count) {
                DeadZombies = 0;
                GenerateWave(time);
            }
        }
    }
}
